Skip to content

feat(rpc): serve states/{id}/fork, config/deposit_contract and validator/duties/sync - #642

Merged
MegaRedHand merged 1 commit into
mainfrom
feat/beacon-api-missing-endpoints
Oct 20, 2026
Merged

MegaRedHand merged 1 commit into
mainfrom
feat/beacon-api-missing-endpoints

Conversation

@pablodeymo

Copy link
Copy Markdown
Collaborator

🗒️ Description / Motivation

Validator clients call Beacon API endpoints that ethlambda beacon didn't serve. This PR adds three of the four:

  • GET /eth/v1/beacon/states/{state_id}/fork (ValidatorRequiredApi): used for fork detection and signing domains. Prysm's validator client calls it in its doppelganger check.
  • GET /eth/v1/config/deposit_contract: network sanity checks and tooling. Prysm's validator client needs it for voluntary exits and its keymanager UI.
  • POST /eth/v1/validator/duties/sync/{epoch} (ValidatorRequiredApi): a validator client's sync committee service. Without it, Lighthouse and Prysm log a warning and skip sync duties.

The fourth, POST /eth/v1/validator/liveness/{epoch}, comes in a separate PR, since it's the only one that needs new shared state.

What Changed

  • crates/net/rpc/src/beacon/states.rs: get_fork. It resolves state_id with the same load() as the other state endpoints and returns state.fork(), plus execution_optimistic and finalized.
  • crates/net/rpc/src/beacon/config.rs: get_deposit_contract. It returns Config::deposit_chain_id and deposit_contract_address, the same values /config/spec reports.
  • crates/net/rpc/src/beacon/validator.rs: post_sync_duties / sync_duties.
  • crates/blockchain/state_transition/src/beacon/helpers/altair.rs: compute_sync_committee_period, as altair's validator.md defines it.
  • Docs: the three routes in the Beacon API table in docs/rpc.md, a note on sync duties, and a new docs/spec_deviations.md entry.

Correctness / Behavior Guarantees

fork

  • A state root is a 404 with the existing "not indexed" message, the same as every other state endpoint.

duties/sync

  • Which committee: an epoch in the head state's own sync committee period reads current_sync_committee; the next period reads next_sync_committee.
  • Refused periods: any other period is a 400.
    • The period after next is refused by the spec.
    • An earlier period is refused deliberately, since it would need a historical state. That's recorded as a spec deviation. Lighthouse and Prysm serve it, but a validator client only asks about the current and next period.
  • Seats: a validator is matched by pubkey, since the committee stores pubkeys, and gets every seat it holds. The committee is drawn with replacement, so one validator can hold several. One pass over the committee builds the map, so the cost doesn't depend on how many validators are requested.
  • Errors:
    • A validator with no seat is left out of data, as the spec says.
    • An unknown index is a 400, as in both Lighthouse and Prysm.
    • 503 while syncing, as the spec and both clients do. The existing duties/attester and duties/proposer don't return 503; that's unchanged here.
  • Known gap: this node doesn't serve the sync committee message or contribution endpoints yet (planned separately). A validator client that gets sync duties here will fail to publish the messages they call for. Today it skips sync duties with a warning; neither way earns the sync rewards.

Tests Added / Run

  • fork:
    • the_fork_is_the_one_the_state_carries
    • the_finalized_states_fork_is_marked_finalized
    • a_fork_by_state_root_is_a_404
  • deposit_contract: the_deposit_contract_is_mainnets
  • duties/sync, on a fixture whose current committee is validators 0–7 and next committee validators 8–15, so reading the wrong one shows up:
    • the_current_period_reads_the_current_committee (also covers a validator in neither committee, and every seat listed)
    • the_next_period_reads_the_next_committee
    • the_period_after_next_is_a_400
    • an_earlier_period_is_a_400
    • an_unknown_validator_is_a_400
    • a_syncing_node_answers_503
  • compute_sync_committee_period: a_sync_committee_period_spans_its_epochs_and_no_more
  • CI's minimal-preset commands pass locally (cargo test -p ethlambda-types --lib --features preset-minimal and the same for ethlambda-state-transition).
  • Not yet run: a kurtosis devnet with Lighthouse's validator client, to check its sync-duties warning goes away.

Related Issues / PRs

  • Liveness follows in a separate PR.
  • The sync committee message and contribution endpoints are PR C/D of the validator API plan.

✅ Verification Checklist

  • Ran make fmt — clean
  • Ran make lint (clippy with -D warnings) — clean
  • Ran make test (test-consensus plus test-node, at release-fast) — all passing (1948 passed, 0 failed, 26 ignored)

@pablodeymo pablodeymo added the beacon Ethereum Beacon Chain client label Oct 1, 2026
@pablodeymo pablodeymo mentioned this pull request Oct 1, 2026
3 tasks done
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

🤖 Claude Code Review

Review: PR 642 (sync duties, fork, and deposit_contract endpoints)

The change is small and well scoped, and the new endpoints are tested. I found no correctness or security bugs. The remaining points are minor.

post_sync_duties (crates/net/rpc/src/beacon/validator.rs)

  • The period selection is correct. Same period as the head uses current_sync_committee, the next period uses next_sync_committee, and anything else is a 400. head_period + 1 can't overflow in practice.
  • Matching validators by pubkey with a single pass over the committee is the right shape. Seats are returned as quoted strings and a validator with several seats gets all of them, as the spec requires.
  • The SyncStatusController extension is already layered onto the beacon router (lib.rs:215), so the handler won't hit a missing-extension 500.
  • Duplicate indices in the request body produce duplicate duty entries. Dedup the indices if you want to match other clients. It is harmless otherwise.
  • An unknown validator index returns 400, which is stricter than some clients. That is consistent with the attester-duties endpoint. Since the PR already documents the earlier-period refusal in spec_deviations.md, one line on this would help.
  • The period is derived from the head state's slot, not the wall clock. If the head lags across a period boundary (empty slots), a request for the new period gets the head's next committee. That is still correct, since it is the committee for the following period. The case is not covered by a test and is not a bug.
  • held.clone() allocates once per matching validator. That is negligible next to the positions map built per request.

get_fork (states.rs)

  • It is consistent with the other state endpoints: same load, execution_optimistic, and finalized handling, and a 0x state root gives a 404. The tests cover quoting, the finalized flag, and the root refusal.

get_deposit_contract (config.rs)

  • It reads from the same Config as /spec, so the two can't disagree. It uses HexPrefixed for the address and to_string() for the quoted chain id, as the spec expects. The test pins mainnet's values.

compute_sync_committee_period (helpers/altair.rs)

  • It is a faithful port of the spec function and has a boundary test. It will panic only if EPOCHS_PER_SYNC_COMMITTEE_PERIOD were 0, which can't happen with the presets.

Docs

  • rpc.md and spec_deviations.md are updated accurately. The note that no sync committee message or contribution endpoint exists yet is useful. A validator client that gets duties here cannot publish what they ask for.

Overall this looks good to merge.


Automated review by Claude (Anthropic) · sonnet · custom prompt

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

🤖 Codex Code Review

Looks good overall. The added endpoints are small, well-scoped, and the sync-duty implementation is efficient enough for the stated constraints. I did not spot consensus-critical bugs in fork choice, finalization, state transition, XMSS, or SSZ paths from this diff.

  • No correctness issue in compute_sync_committee_period; the helper and boundary test in crates/blockchain/state_transition/src/beacon/helpers/altair.rs:173 are straightforward and spec-aligned.
  • GET /eth/v1/beacon/states/{state_id}/fork in crates/net/rpc/src/beacon/states.rs:106 correctly reuses existing state resolution and preserves the usual execution_optimistic/finalized metadata.
  • GET /eth/v1/config/deposit_contract in crates/net/rpc/src/beacon/config.rs:37 is low risk and serializes the expected fields in Beacon API format.
  • POST /eth/v1/validator/duties/sync/{epoch} in crates/net/rpc/src/beacon/validator.rs:93 has sensible gating:
    • rejects while syncing,
    • rejects pre-Altair states,
    • limits queries to current/next sync-committee period,
    • handles repeated committee seats correctly by pubkey.

One small maintainability/perf note:

  • crates/net/rpc/src/beacon/validator.rs:138 builds HashMap<BlsPubkey, Vec<String>>, converting positions to strings eagerly. Since only matched validators are returned, storing Vec<usize>/Vec<u64> first and stringifying only when building SyncDuty would avoid some unnecessary allocation. Not a blocker, just a small cleanup.

No security concerns jumped out from this PR.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

🤖 Kimi Code Review

I'll review this PR systematically across correctness, security, performance, and Rust best practices.

Overall Assessment

This PR adds three Beacon API endpoints (/eth/v1/config/deposit_contract, /eth/v1/beacon/states/{state_id}/fork, /eth/v1/validator/duties/sync/{epoch}) and a helper function. The code is generally well-structured with good test coverage, but I found several issues.


Detailed Findings

1. Critical: Integer Overflow Risk in compute_sync_committee_period

File: crates/blockchain/state_transition/src/beacon/helpers/altair.rs, line 173-175

pub fn compute_sync_committee_period(epoch: Epoch) -> u64 {
    epoch / preset::EPOCHS_PER_SYNC_COMMITTEE_PERIOD
}

Epoch is typically u64, and EPOCHS_PER_SYNC_COMMITTEE_PERIOD is 256 on mainnet. The division is safe, but the function lacks documentation on expected input bounds. If Epoch were ever a smaller type, this could panic. More critically, callers multiplying back (as seen in tests) can overflow:

File: crates/net/rpc/src/beacon/validator.rs, lines 1004-1005

let next_period_epoch = preset::EPOCHS_PER_SYNC_COMMITTEE_PERIOD
    * (compute_sync_committee_period(compute_epoch_at_slot(state.slot())) + 1);

This multiplication can overflow u64 if compute_sync_committee_period returns a large value. While practically unlikely with mainnet parameters, defensive coding is warranted.

Suggestion: Add checked_mul or document the invariant. Consider saturating or checked arithmetic for the multiplication in validator.rs.


2. Bug: Incorrect Error Type for Pre-Altair States

File: crates/net/rpc/src/beacon/validator.rs, line 86

let (current, next) = state
    .sync_committees()
    .map_err(|_| ApiError::BadRequest("sync committees start at altair"))?;

This returns 400 Bad Request when sync committees don't exist (pre-Altair state). Per the Beacon API specification, this should be 500 Internal Server Error or at minimum not 400—the client didn't send a bad request, the server has a state that doesn't support this feature. Actually, re-reading: if a validator client requests a sync duty for a pre-Altair epoch, that's arguably a client error. However, if the head state is pre-Altair due to the node's own sync position, this is misleading.

Suggestion: Verify against spec. If the node is on a pre-Altair fork and receives this request, 400 may be acceptable, but consider if 500 or a more specific error is appropriate.


3. Performance: Unnecessary Clone in sync_duties

File: crates/net/rpc/src/beacon/validator.rs, line 107

validator_sync_committee_indices: held.clone(),

held is &Vec<String> from the HashMap, and SyncDuty stores Vec<String>. The clone is necessary for the return value, but consider if SyncDuty could use Arc<[String]> or if the positions could be returned as slices. Given the API response needs owned data, this is acceptable but worth noting for high-throughput scenarios.

More concerning is the eager construction of the entire positions map:

File: crates/net/rpc/src/beacon/validator.rs, lines 95-100

let mut positions: HashMap<BlsPubkey, Vec<String>> = HashMap::new();
for (position, pubkey) in committee.pubkeys.iter().enumerate() {
    positions
        .entry(*pubkey)
        .or_default()
        .push(position.to_string());
}

This builds a map for all validators in the committee (512 on mainnet) even if only a few indices are requested. For typical validator clients requesting 1-100 duties, this is wasteful.

Suggestion: For small request sets, linear scan the committee per validator instead. The break-even depends on typical request sizes, but consider benchmarking or using a threshold-based approach.


4. Correctness: compute_sync_committee_period Not Using Spec's uint64 Division Semantics

File: crates/blockchain/state_transition/src/beacon/helpers/altair.rs, line 173-175

The spec defines compute_sync_committee_period as epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD using Python's floor division. Rust's / on unsigned integers is floor division, so this matches. However, the function should explicitly handle or document behavior for Epoch types that might be signed.

Acknowledgment: The test at lines 606-612 correctly verifies boundary conditions. Good coverage.


5. Security: Potential DoS via Large Request Body

File: crates/net/rpc/src/beacon/validator.rs, line 72-76

async fn post_sync_duties(
    // ...
    Json(indices): Json<Vec<String>>,
) -> Response {

There's no apparent limit on indices length. A malicious client could send a very large array, causing:

  • Excessive memory allocation in JSON parsing
  • Long iteration time in sync_duties
  • Large response construction

Suggestion: Add a maximum length check for indices, or use Axum's body size limits. Compare with existing post_attester_duties for consistency.


6. Error Handling: Inconsistent Validator Lookup Error

File: crates/net/rpc/src/beacon/validator.rs, line 104-106

let validator = state
    .validator(validator_index)
    .map_err(|_| ApiError::BadRequest("unknown validator index"))?;

This returns 400 for an unknown validator index. However, the Beacon API specification typically expects 404 Not Found or 400 depending on context. Verify consistency with other endpoints in this file.

Looking at post_attester_duties for comparison—check if it uses the same pattern. If inconsistent, standardize.


7. Code Quality: Default::default() for SyncStatusController in Tests

File: crates/net/rpc/src/beacon/validator.rs, multiple test lines (e.g., 978, 993, 1007, etc.)

post_sync(state.clone(), epoch, &["3", "9", "20"], Default::default()).await;

Default::default() for SyncStatusController presumably creates a "not syncing" state. This is implicit and fragile—if Default changes, tests break semantically without compilation errors.

Suggestion: Use an explicit constructor like SyncStatusController::new(SyncStatus::NotSyncing) for clarity, matching the a_syncing_node_answers_503 test pattern.


8. Documentation: fork Endpoint Version Inconsistency

File: docs/rpc.md, line 233

| `GET` | `/eth/v1/beacon/states/{state_id}/fork` | JSON | That state's `Fork`: previous and current version, and the epoch it changed |

The implementation uses v1:

File: crates/net/rpc/src/beacon/states.rs, line 40

.route("/eth/v1/beacon/states/{state_id}/fork", get(get_fork))

But the standard Beacon API has /eth/v1/beacon/states/{state_id}/fork in the spec—this appears correct. However, verify the response format matches spec exactly, including the execution_optimistic and finalized metadata fields.


9. Test Coverage: Missing Test for execution_optimistic in get_fork

File: crates/net/rpc/src/beacon/states.rs, lines 457-489

The the_fork_is_the_one_the_state_carries test checks execution_optimistic is a boolean, but doesn't verify it matches store.is_beacon_optimistic(root) correctly. The the_finalized_states_fork_is_marked_finalized test only checks finalized.

Suggestion: Add a test verifying execution_optimistic propagates correctly from the store, or at least matches the expected value for the test fixture.


10. Rust Idiom: Unnecessary format! in Test Assertion

File: crates/net/rpc/src/beacon/validator.rs, line 983

assert_eq!(duties[0]["pubkey"], format!("0x{}", hex::encode(pubkey.0)));

This creates a hex string manually. If pubkey implements Serialize with a hex format, use that instead. If not, consider whether serde_json::to_value(pubkey) would work, or if a helper exists.

Actually, looking more carefully: pubkey.0 suggests accessing internal bytes. This is fragile if the struct layout changes.

Suggestion: Use pubkey.to_string() or a dedicated formatting method if available, or serde_json::to_value(&pubkey) to match the JSON serialization path.


11. Memory Safety: hex::encode(pubkey.0) Assumes Fixed-Size Array

File: crates/net/rpc/src/beacon/validator.rs, line 983

pubkey.0 is presumably a fixed-size byte array (48 bytes for BLS12-381). hex::encode handles this correctly, but this pattern appears ad-hoc.

Suggestion: Ensure BlsPubkey has a consistent serialization approach. If serde with serialize_with is used elsewhere, use that for test assertions to avoid drift.


12. Spec Compliance: get_deposit_contract Response Format

File: crates/net/rpc/src/beacon/config.rs, lines 38-46

crate::json_response(serde_json::json!({
    "data": {
        "chain_id": config.deposit_chain_id.to_string(),
        "address": HexPrefixed(&config.deposit_contract_address).to_string(),
    }
}))

Verify chain_id should be string or number per spec. The .to_string() makes it a string. Check Beacon API spec—typically chain_id is a Uint64 quoted string in JSON, which this matches.

The address uses HexPrefixed—verify this produces lowercase hex with 0x prefix, as spec requires.


13. Concurrency: sync_status.get() Race Condition?

File: crates/net/rpc/src/beacon/validator.rs, line 78

if sync_status.get() == SyncStatus::Syncing {

This reads a shared atomic or lock. The value could change immediately after this check, before sync_duties executes. This is a TOCTOU (time-of-check-time-of-use) issue, but likely benign here since:

  • Worst case: node starts syncing during request, serves slightly stale data
  • The spec doesn't require strict linearizability for this endpoint

Acknowledgment: This pattern is common in this codebase and acceptable for this use case.


14. Missing Import Check

File: crates/net/rpc/src/beacon/validator.rs, line 17

use ethlambda_blockchain::{SyncStatusController, metrics::SyncStatus};

Verify SyncStatusController is Clone or Copy as needed for the Extension layer. The tests use Default::default() and explicit construction, suggesting it implements appropriate traits.


Positive Observations

  1. Good test coverage for boundary conditions in compute_sync_committee_period
  2. Clear documentation of intentional spec deviations in docs/spec_deviations.md
  3. Consistent error handling pattern with ApiError
  4. Efficient committee lookup using HashMap instead of O(n×m) scanning (though see Point 3 for trade-off)
  5. Proper SSZ/JSON serialization helpers used consistently

Summary Table

Priority Item File Line
High Overflow risk in period multiplication validator.rs ~1004
Medium Potential DoS via unbounded indices validator.rs 72-76
Medium Inefficient eager HashMap construction validator.rs 95-100
Low Implicit Default::default() in tests validator.rs multiple
Low hex::encode(pubkey.0) fragility validator.rs 983
Low Missing execution_optimistic test states.rs 457-489

The PR is generally well-crafted and ready for merge after addressing the overflow risk (Item 1) and considering the DoS limit (Item 2).


Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt

@MegaRedHand
MegaRedHand added this pull request to stack #654 October 2, 2026 19:56

@MegaRedHand MegaRedHand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments, nothing blocking. The period bound is the only behavior change I'd consider; the rest are small cleanups and tighter tests.

let requested_period = compute_sync_committee_period(epoch);
let committee = if requested_period == head_period {
current
} else if requested_period == head_period + 1 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the allowed periods are measured from the head state's slot, not the clock. When the head is still in period P but the clock is already in P+1 (an empty first slot of the new period, or a head a few slots behind), a request for P+2 is a valid "next period" request and gets a 400 here.

Lighthouse covers this by advancing a lagging head state to the current period before answering (duties_from_state_load in http_api/src/sync_committees.rs). Here that would mean advancing through fork_choice::checkpoint_state, as attestation_data already does, since P+2's committee only exists in the advanced state's next_sync_committee.

for validator_index in indices {
let validator = state
.validator(validator_index)
.map_err(|_| ApiError::BadRequest("unknown validator index"))?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: a 400 for an index the state doesn't know matches Lighthouse, but attester_duties in this same file skips unknown indices and answers 200. Worth picking one behavior for both duty endpoints, so a validator client sending the same index set to both gets consistent answers?

indices: &[String],
) -> Result<serde_json::Value, ApiError> {
let epoch = parse_epoch(epoch)?;
let indices = indices

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this parse block is the same as attester_duties', which collects into a HashSet and so also drops duplicates. Here ["3", "3"] returns validator 3's duty twice. A shared parse_indices helper next to parse_epoch would keep both in line.


// One pass over the committee rather than one per requested validator:
// the committee stores pubkeys, so that is what a validator is matched by.
let mut positions: HashMap<BlsPubkey, Vec<String>> = HashMap::new();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this allocates a String per seat and then clones the Vec for each duty. Storing Vec<u64> positions and serializing the field with serialize_with = "ethlambda_types::beacon::serde_helpers::quoted_u64_seq::serialize" (as the altair containers do) avoids the per-seat allocations; once the requested indices are deduplicated, remove instead of get + clone avoids the copy too.

Comment thread docs/rpc.md
state; see `docs/spec_deviations.md`). A validator is matched by pubkey and
gets every seat it holds, since the committee is drawn with replacement; one
with no seat is left out. An unknown index is a `400`, and the endpoint is a
`503` while the node is syncing. This node serves no sync committee message

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: besides the message and contribution endpoints, POST /eth/v1/validator/sync_committee_subscriptions is also unserved. Once a validator client gets sync duties it starts calling it, so for a VC with a sync committee member the visible effect of this PR is more 404s (subscriptions per epoch, messages and contributions per slot) instead of one 404 on duties/sync. Worth listing the subscriptions gap here too.


/// `GET /eth/v1/beacon/states/{state_id}/fork`: the `Fork` the state carries,
/// which is what a validator client builds its signing domains from.
async fn get_fork(Path(state_id): Path<String>, State(store): State<Store>) -> Response {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the load, error and execution_optimistic + finalized envelope here is the same as get_finality_checkpoints'. A small helper that takes a closure over the state (state_json(store, state_id, |state| data)) would keep the state sub-resources consistent as more are added.

crate::json_response(serde_json::json!({
"data": {
"chain_id": config.deposit_chain_id.to_string(),
"address": HexPrefixed(&config.deposit_contract_address).to_string(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: hex_string(config.deposit_contract_address), defined further down in this file, does the same thing.

let response = app.oneshot(request).await.unwrap();
let status = response.status();
let body = response.into_body().collect().await.unwrap().to_bytes();
(status, serde_json::from_slice(&body).unwrap_or_default())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test nit: unwrap_or_default() turns a non-JSON body into Null, and the 400 tests only check the status code, so they would still pass if a different 400 path fired (epoch parse, body rejection). Unwrapping the parse and asserting on json["message"] would pin each test to the check it names.

}

#[tokio::test]
async fn the_current_period_reads_the_current_committee() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test gap: every current-period test asks for exactly the head's epoch. A case for a later epoch in the same period (for example head_epoch + 1) would catch a regression to comparing epochs instead of periods.

@MegaRedHand
MegaRedHand force-pushed the feat/beacon-api-missing-endpoints branch from a7f7f83 to b252fbc Compare October 5, 2026 19:01
MegaRedHand added a commit that referenced this pull request Oct 5, 2026
…poser-duties-v2

#642 was squashed onto the current beacon-chain-integration, so this merge
ran against the old base and both sides re-added #642's work. Git kept the
`duties/sync` test module twice; the second copy is dropped. In docs/rpc.md
the duties paragraph keeps this branch's `dependent_root` wording.
@MegaRedHand
MegaRedHand force-pushed the feat/beacon-api-missing-endpoints branch from b252fbc to 49359e3 Compare October 7, 2026 18:36
Base automatically changed from beacon-chain-integration to main October 20, 2026 16:19
…eposit_contract and POST /eth/v1/validator/duties/sync/{epoch} from ethlambda beacon, three of the four Beacon API endpoints validator clients call that were missing. fork returns the state's own Fork through the same state_id resolution as the other state endpoints, so a state root is the same 404. deposit_contract returns the Config's deposit chain id and contract address. duties/sync reads the head state's current_sync_committee for an epoch in the head's own sync committee period and next_sync_committee for the next one, matches each requested validator by pubkey and returns every seat it holds (the committee is drawn with replacement), leaves out validators with no seat, and answers 400 for an unknown index or any other period and 503 while syncing. An earlier period is refused rather than answered from a historical state, recorded in docs/spec_deviations.md. compute_sync_committee_period is added to the altair helpers as validator.md defines it.
@MegaRedHand
MegaRedHand force-pushed the feat/beacon-api-missing-endpoints branch from 49359e3 to f6c5793 Compare October 20, 2026 20:46

@MegaRedHand MegaRedHand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revisit these nits after merging everything. Some of the things I mentioned were already fixed in following PRs

@MegaRedHand
MegaRedHand added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 13578d6 Oct 20, 2026
12 checks passed
@MegaRedHand
MegaRedHand deleted the feat/beacon-api-missing-endpoints branch October 20, 2026 21:24
MegaRedHand added a commit that referenced this pull request Oct 21, 2026
…#634)

Squash of feat/beacon-gloas at 1d667c4 (merge-base 35c074b).

Adapted onto #626-#633 and main (#642-#677):
- #632/#633's epoch single pass ported to the element accessors; fulu runs
  through the single pass, with the eth1-bridge gate on electra only.
- DB_VERSION 6 (gloas Config 5 + #628's table).
- compute_node_weights skips the boost outside the justified subtree
  (boost_reaches_the_descent), for #627's LiveChain prune; the
  boosted-finalized-block bound test is dropped and the differential tests
  skip pruned roots.
- #628's set_unrealized_justification slot argument in the tests;
  get_forkchoice_store keeps the anchor block's slot.
- #653's validator-by-id handler on iter_validators/balance.
MegaRedHand added a commit that referenced this pull request Oct 21, 2026
Squash of feat/beacon-gloas-live at d1ef2fe (merge-base d91c6e5).

Adapted onto #626-#638 and main (#642-#677):
- 13 tables (#628's BeaconUnrealizedJustifications beside the gloas two).
- #626's event bus, test stand-ins and BlockAnnouncement carry the gloas
  sidecar and the two gloas chain-actor messages.
- The Beacon API's optimism readers (syncing, fork, validator-by-id, sync
  duties, require_validated) go through block_is_optimistic.
- #643's liveness recorder ignores envelopes and payload attestations.
MegaRedHand added a commit that referenced this pull request Oct 21, 2026
…ient (#668)

Squash of feat/beacon-sync-committee at 07f0357 (merge-base 55adf1b).

Adapted onto #643-#667: gloas block production packs an empty sync
aggregate, #643's liveness recorder ignores sync committee messages, and
#638's per-validator settings reach the sync duties. The #642 commit this
branch carries is main's already.
MegaRedHand added a commit that referenced this pull request Oct 21, 2026
Squash of fix/beacon-sync-duties-wall-clock at 09ee55a (on its #642 commit f6c5793,
which main carries as 13578d6).

Adapted onto #663: the sync duties share its state_for_epoch and
duties_response rather than carrying a copy.
zexoverz pushed a commit to zexoverz/ethlambda that referenced this pull request Oct 22, 2026
## 🗒️ Description / Motivation

Merges `beacon-chain-integration` into `main`. This adds an **Ethereum
Beacon Chain** client (phase0 through fulu) to the ethlambda binary,
alongside the Lean consensus client:

- **`ethlambda beacon`**: a beacon node. It checkpoint-syncs, follows
the chain over gossip and req/resp, validates execution payloads through
an execution client's Engine API, and serves the Beacon API, including
the validator endpoints.
- **`ethlambda validator`**: a beacon validator client (attest,
aggregate, propose) that talks to any beacon node over the standard REST
API.
- **`ethlambda node`** (lean) keeps its behavior, apart from the items
under "Behavior changes for lean" below.

Both chains run on the same storage layer, P2P actor, RPC crate and
chain actor. `BeaconState` has a `Lean` variant holding lean's `State`,
and each shared handler dispatches on it once, at the top.

Each piece was reviewed as its own PR into the integration branch
(listed under "Related Issues / PRs"). This PR merges that branch.

## What Changed

| Area | Change |
|---|---|
| `ethlambda-types` (`beacon/`) | Containers for phase0 to fulu, presets
(compile-time `preset-minimal` feature), runtime fork config, and a
`Lean` variant on `ForkName` and `BeaconState` |
| `ethlambda-state-transition` (`beacon/`) | State transition, epoch
processing, fork upgrades, fork choice, BLS (`blst`), KZG (`c-kzg`),
gossip validation rules, DAS custody math |
| `ethlambda-ssz-tree` (**new**) | Persistent Merkle `List`/`Vector` for
the validator registry and balances, so consecutive states share
unchanged subtrees |
| `ethlambda-storage` | One `Store` for both chains. Beacon states are
stored as snapshots plus VCDIFF deltas and written on a background
thread. Adds a committee cache, and `db_version`/`chain`/`preset`
metadata that resume checks |
| `ethlambda-blockchain` | Beacon path in the chain actor
(`BlockChain::spawn_beacon`): import, holding blocks until their parent
or columns arrive, applying gossip aggregates to fork choice, block
production and aggregation |
| `ethlambda-engine` (**new**) | Engine API client with JWT auth:
`newPayload`, `forkchoiceUpdated`, `getPayload`, `getBlobs` |
| `ethlambda-p2p` | Beacon wire: blocks, aggregates, subnet attestations
and data columns are validated before relay;
`beacon_blocks_by_{range,root}/2` served and requested; range sync;
column custody and sampling; attestation subnet backbone; discv5 always
on for `beacon` |
| `ethlambda-rpc` | Beacon API under `/eth/v1` and `/eth/v2`: JSON by
default, SSZ on request. The router is picked by `Store::chain()`, so
`/lean/v0` and the Beacon API are never served together |
| `ethlambda-validator` (**new**) | Validator client: duties,
attestations, aggregation selection, proposals, keymanager API. It has
**no slashing protection** (recorded in `docs/spec_deviations.md`) |
| `bin/ethlambda` | `beacon` and `validator` sub-commands; `--network`
takes a built-in name (`mainnet`, `sepolia`, `hoodi`) or a directory of
published network files; beacon checkpoint sync; `benchmark import`
replays a real block corpus offline; `run_node` is one startup path for
both chains |
| CI / Makefile | One beacon spec-test job per preset, minimal-preset
check, `make test` split into `test-consensus` and `test-node` (one
runner's disk no longer fits both), benchmark-smoke and tooling jobs,
disk-cleanup action |
| Docs | New: `beacon_stf.md`, `beacon_wire.md`, `beacon_engine.md`,
`cli.md`. Updated: `data_storage.md`, `metrics.md`, `rpc.md`,
`checkpoint_sync.md`, `discovery.md`, `benchmarking.md`,
`spec_deviations.md`, README (beacon quickstart with ethrex) |

## Correctness / Behavior Guarantees

- **Separation:** beacon behavior lives in `beacon` modules that lean
code never reads, and those modules never read lean's. The shared crates
dispatch on the chain once per handler.
- **Lean state at a beacon accessor:** this panics, naming the function
it reached (`lean_state_unreachable` / `lean_fork_unreachable`), so the
caller never gets a wrong answer back. `ForkName::Lean` is left out of
`ForkName::ALL`, so neither `parse` nor a fork upgrade can reach it.
- **Spec conformance:** every consensus-specs `v1.6.1` fixture case
passes on both presets: 5705 mainnet and 40009 minimal, including 150
mainnet `fork_choice` cases. Gossip fixtures are pinned to
`v1.7.0-beta.1`.
- **Resume safety:** a data directory written by another chain, preset
or format version is refused at startup, before any state is read.

### ⚠️ Behavior changes for lean (`ethlambda node`)

- **Existing data directories are refused on resume.** A directory
written by `main` has no `db_version` key, so `Store::from_db_state`
returns `DbVersionMismatch { found: 0, expected: 4 }`. There is no
migration: an upgraded node needs a fresh data directory (or checkpoint
sync).
- `blst` and `c-kzg` are now dependencies of the lean binary too, since
the state-transition crate holds both chains' rules. They are not
feature-gated.
- libssz is a git dependency pinned by `rev`: the beacon containers need
`DerefMut`/`IndexMut` on `SszList` (lambdaclass/libssz#33), which no
crates.io release has yet.
- The `release-fast` profile (used for tests) now sets `debug-assertions
= true`.
- `P2P::spawn` now runs before the chain actor is spawned, so gossip
arriving in that short window is dropped.
- `/lean/v0/blocks/finalized` answers JSON when asked for it by name.
SSZ stays the default.

## Tests Added / Run

- **Beacon spec tests:** `make test-beacon` (or `test-beacon-mainnet` /
`test-beacon-minimal`). Each fixture case is a separately named test,
e.g. `cargo test -p ethlambda-state-transition --test beacon_spec_tests
--features beacon-spec-tests -- electra/attester_slashing`. CI runs one
job per preset.
- **`make test`** still needs no consensus-spec download: the beacon
spec target is behind the `beacon-spec-tests` feature, and the BLS/KZG
vector tests report as ignored without it.
- **Minimal preset:** the `Test minimal preset` CI job builds and runs
the `ethlambda-types` unit tests with `preset-minimal` on.
- **Benchmark smoke:** a CI job runs the `benchmark` sub-command end to
end.
- Unit and integration tests in every new crate (`ssz-tree`, `engine`,
`validator`) and in the beacon paths of storage, p2p, rpc and the chain
actor.

## Related Issues / PRs

<details>
<summary>PRs merged into the integration branch (reviewed on
<code>lambdaclass/ethlambda_private</code>)</summary>

**Types and state transition**
- lambdaclass/ethlambda_private#1 Beacon Chain containers under a
`beacon` namespace
- lambdaclass/ethlambda_private#2 Beacon Chain state transition, phase0
through fulu
- lambdaclass/ethlambda_private#37 keep the registry and balances in
persistent Merkle trees

**Follower, sync and storage**
- lambdaclass/ethlambda_private#3 follow mainnet gossip behind an
`ethlambda beacon` sub-command
- lambdaclass/ethlambda_private#6 unify the lean and beacon storage
layer
- lambdaclass/ethlambda_private#7 checkpoint sync for the beacon chain
- lambdaclass/ethlambda_private#8 serve beacon blocks by range and by
root
- lambdaclass/ethlambda_private#9 give the follower a chain, from the
anchor to the tip
- lambdaclass/ethlambda_private#11 fulu data availability sampling,
custody through the availability gate
- lambdaclass/ethlambda_private#13 validate execution payloads over the
Engine API
- lambdaclass/ethlambda_private#15 follow a network described by files
on disk
- lambdaclass/ethlambda_private#26 add sepolia and hoodi as built-in
networks
- lambdaclass/ethlambda_private#48, lambdaclass/ethlambda_private#52,
lambdaclass/ethlambda_private#53, lambdaclass/ethlambda_private#54: held
blocks, their columns and range batches

**P2P**
- lambdaclass/ethlambda_private#18 stop outbound req/resp requests
crossing protocols
- lambdaclass/ethlambda_private#23 rank dial candidates by custody
redundancy
- lambdaclass/ethlambda_private#28 count why peers drop us
- lambdaclass/ethlambda_private#38 validate beacon gossip before
propagating it
- lambdaclass/ethlambda_private#43 run every data column check in p2p
- lambdaclass/ethlambda_private#47 validate beacon aggregates and subnet
attestations before relaying
- lambdaclass/ethlambda_private#19 apply gossip aggregates to fork
choice, backbone two attestation subnets

**API and validator**
- lambdaclass/ethlambda_private#21 serve the Beacon API from `ethlambda
beacon`
- lambdaclass/ethlambda_private#44 report the preset and constants from
`/eth/v1/config/spec`
- lambdaclass/ethlambda_private#16 beacon-chain validator client
- lambdaclass/ethlambda_private#34 serve the validator API
- lambdaclass/ethlambda_private#40 aggregate and propose through
`ethlambda beacon`
- lambdaclass/ethlambda_private#56 pack other nodes' attestations into
produced blocks

**Performance and metrics**
- lambdaclass/ethlambda_private#10, lambdaclass/ethlambda_private#20,
lambdaclass/ethlambda_private#22, lambdaclass/ethlambda_private#24,
lambdaclass/ethlambda_private#33, lambdaclass/ethlambda_private#49:
registry scans, parallel pubkey validation, committee cache, background
state writes, BLS key validation
- lambdaclass/ethlambda_private#17, lambdaclass/ethlambda_private#31:
import timing by phase
- lambdaclass/ethlambda_private#25 benchmark block import by replaying a
real corpus

**CI**
- lambdaclass/ethlambda_private#12, lambdaclass/ethlambda_private#30,
lambdaclass/ethlambda_private#45: split lint/test jobs, benchmark smoke
job, runner selection

</details>

Open PRs based on `beacon-chain-integration`, **not** included here:
lambdaclass#626, lambdaclass#627, lambdaclass#629, lambdaclass#630, lambdaclass#632, lambdaclass#634, lambdaclass#636, lambdaclass#638, lambdaclass#642, lambdaclass#644 to lambdaclass#652,
lambdaclass#656, lambdaclass#658.

## ✅ Verification Checklist

- [ ] Ran `make fmt` (clean)
- [ ] Ran `make lint` (clippy with `-D warnings`, clean)
- [ ] Ran `make test` (`test-consensus` plus `test-node`, at
`release-fast`), all passing
- [ ] Ran `make test-beacon` (both presets), all passing

---------

Co-authored-by: Pablo Deymonnaz <pdeymon@fi.uba.ar>
Co-authored-by: Klaus Lungwitz <klaus.lungwitz@lambdaclass.com>
Co-authored-by: Klaus @ LambdaClass <18153834+klaus993@users.noreply.github.com>
Co-authored-by: Chase Wright <chase.wright@ethereum.org>
zexoverz pushed a commit to zexoverz/ethlambda that referenced this pull request Oct 22, 2026
## 🗒️ Description / Motivation

`POST /eth/v1/validator/liveness/{epoch}` is what a validator client's
**doppelganger protection** calls before it signs anything: "did anyone
see these validators act in epoch E?" If the node answers 404:

- **Lighthouse's validator client** with doppelganger protection on
keeps its validators muted forever.
- **Prysm's** fails at startup.

This is the last of the four missing endpoints; the other three are in
lambdaclass#642, which this PR is stacked on.

The Beacon API leaves the answer to the node's own view ("network, chain
or API"), and clients differ:

| | Source of `is_live` | Current epoch |
|---|---|---|
| Prysm | State participation flags only | Late by however long
inclusion takes |
| Lighthouse | Its gossip, block and aggregator caches | Complete, but
relies on caches that on this node only cover the subnets it joined |
| **This PR** | Participation flags **OR** what this node observed |
Covered as soon as the node sees the validator act |

## What Changed

- **`crates/storage/src/liveness.rs` (new)**: `ObservedLiveness`, one
bitset per epoch for the newest three.
  - An epoch older than the window is ignored.
- Indices are capped at `1 << 24`, so one epoch is at most 2 MiB. Every
writer passes a validated index anyway; mainnet is ~2.4M validators,
about 300 KB.
- **`crates/storage/src/store.rs`**: an `observed_liveness` field and
accessor, shared by every `Store` clone like `committee_cache`, because
the set has three writers and a reader that all already hold a `Store`.
- **Writers:**
- **`crates/net/p2p/src/beacon/verdict.rs`**: on `Accept`, an aggregate
marks its aggregator and every attester its signature verified; a subnet
attestation marks its attester.
- **`crates/blockchain/src/lib.rs`**: a successfully imported beacon
block marks its proposer. This is done at import, not at gossip
acceptance, so range-synced blocks and blocks published through this
node's API count too.
- **`crates/net/rpc/src/beacon/pool.rs`**: submissions through
`pool/attestations` and `aggregate_and_proofs`, since gossip never
delivers a node its own messages. The aggregate path now keeps the
attesting indices that `stateful_checks` already returned.
- **`crates/net/rpc/src/beacon/validator.rs`**: `post_liveness` /
`liveness`.
- **`docs/rpc.md`**: the route and the liveness semantics.

## Correctness / Behavior Guarantees

- **Nothing unverified counts.** Only `Accept`ed gossip and validated
API submissions are recorded; an `Ignore`, `Reject` or `Overloaded`
object marks nobody.
- **Participation is read from the head state** for its own epoch
(`current_epoch_participation`) and the one before
(`previous_epoch_participation`). Before altair there are no flags, so
only observations count.
- **Epoch window:** the *store clock's* previous, current and next
epoch, not the head state's. The next epoch is answered but always
`false`, because a doppelganger check at an epoch boundary can land on
it; Lighthouse accepts it too.
- **Errors:**
  - An epoch outside the window is a 400.
- An unknown index is a 400, as in Prysm (Lighthouse answers `false`).
An index outside the registry is a client bug, better reported than
hidden.
  - **503 while syncing**, as the spec lists; neither client returns it.
- **Lock cost.** Writers take a `Mutex` once per accepted gossip object,
to set bits. That includes every accepted attestation on the backbone
subnets, which don't touch the attestation pool: roughly a couple of
thousand short locks a slot on mainnet, against a reader that only runs
when a validator client asks.

## Tests Added / Run

- **`ObservedLiveness`:**
  - `a_recorded_validator_is_live_in_that_epoch_only`
  - `record_all_sets_every_index`
  - `epochs_older_than_the_window_are_pruned`
  - `an_epoch_below_the_window_is_not_recorded`
  - `an_index_past_the_guard_is_ignored`
  - `observed_liveness_is_shared_across_store_clones`
- **Endpoint** (state at epoch 5 with its store clock set to match):
- `a_participation_flag_makes_a_validator_live` (current and previous
epoch flags)
  - `an_observed_validator_is_live_without_a_flag`
  - `the_next_epoch_is_answered_and_nobody_is_live_in_it`
  - `epochs_outside_the_window_are_a_400`
  - `an_unknown_validator_is_a_400`
  - `a_syncing_node_answers_503`
- **P2P writers:**
  - `an_accepted_aggregate_marks_its_aggregator_and_attesters_live`
  - `an_accepted_subnet_attestation_marks_its_attester_live`
  - `an_object_that_was_not_accepted_marks_nobody_live`
- **RPC writers:**
  - `a_published_attestation_marks_its_attester_live`
  - `a_refused_attestation_marks_nobody_live`
- `a_published_aggregate_marks_its_aggregator_and_attesters_live`: the
aggregate is built on one node and submitted to a fresh one, so only the
aggregate itself can have marked its attesters.
- **Not covered by a unit test:** the proposer write on block import.
The chain actor's tests have no successful beacon import to extend,
since that needs a block passing the full state transition. It's one
line, exercised on a devnet.
- CI's minimal-preset commands pass locally.
- **Not yet run:** kurtosis with Lighthouse's validator client and
`--enable-doppelganger-protection`. Its validators should come online
after the doppelganger wait instead of staying muted.

## Related Issues / PRs

- Stacked on lambdaclass#642 (`fork`, `deposit_contract`, `duties/sync`); retarget
to `beacon-chain-integration` once lambdaclass#642 merges.

## ✅ Verification Checklist

- [x] Ran `make fmt` — clean
- [x] Ran `make lint` (clippy with `-D warnings`) — clean
- [x] Ran `make test` (`test-consensus` plus `test-node`, at
`release-fast`) — all passing (1966 passed, 0 failed, 26 ignored)
zexoverz pushed a commit to zexoverz/ethlambda that referenced this pull request Oct 22, 2026
…r duties, for Lighthouse's validator client (lambdaclass#653)

## 🗒️ Description / Motivation

Lighthouse's validator client (v8.2.3) can't run on ethlambda beacon,
because it needs two endpoints this node doesn't serve:

| Endpoint | Before | What Lighthouse's VC does on the 404 |
|---|---|---|
| `GET /eth/v1/beacon/states/{state_id}/validators/{validator_id}` | 404
(only `?id=` and `POST` served) | It resolves each key's index through
this route. Without it, every validator is "inactive", so no attester,
sync, liveness or proposer duty is ever requested |
| `GET /eth/v2/validator/duties/proposer/{epoch}` | 404 (v1 served) | It
asks for v2 only, with no fallback to v1, so "Failed to download
proposer duties" every epoch and it never proposes |

The first one blocks the end-to-end check lambdaclass#642 and lambdaclass#643 list as still to
do: Lighthouse's validator client with
`--enable-doppelganger-protection`. Its doppelganger service only calls
`liveness` for validators it has indices for.

Found on a kurtosis devnet with Lighthouse's validator client talking
only to ethlambda beacon. The VC logged `Validator without index` for
each of its 64 keys and `All validators inactive` every slot.

## What Changed

- **`crates/net/rpc/src/beacon/states.rs`**: `GET
/eth/v1/beacon/states/{state_id}/validators/{validator_id}`. It returns
one registry entry as an object, by index or by pubkey: the same entry
the list endpoint returns, without the array.
- **`crates/net/rpc/src/beacon/validator.rs`**: `GET
/eth/v2/validator/duties/proposer/{epoch}`. These are v1's duties with
v2's `dependent_root`. v1 and v2 share one handler, parameterized by the
dependent-root rule.
- **`docs/rpc.md`**: both routes, and the v2 `dependent_root` rule.
- **`tooling/kurtosis-validator/`**:
- `ethlambda_validator.client: lighthouse` runs `lighthouse vc` in place
of ethlambda's validator client, on the same derived keys and against
the same beacon node list.
- Lighthouse writes its definitions file into the validators directory,
so it gets a writable copy of the keys.
    - `doppelganger: true` adds `--enable-doppelganger-protection`.
    - `lighthouse_extra_params` passes any other flags through.
- `network_params_lighthouse_vc.yaml` runs it against ethlambda beacon
alone, with doppelganger protection on.
- **`bin/ethlambda/src/main.rs`**: `cargo fmt`. A merge of
`beacon-chain-integration` left the module list out of order, which is
what fails Lint on lambdaclass#642 and lambdaclass#643.

## Correctness / Behavior Guarantees

- **`validators/{validator_id}`:**
- `404` for a validator the state doesn't hold, `400` for a malformed
id.
  - `state_id` resolves the same way as on every other state endpoint.
- **v2 proposer duties:**
- `dependent_root` is the block root at the slot before the **previous**
epoch's first slot, since fulu fixes proposers an epoch ahead. Where
that would underflow, it's the genesis block root.
  - `503` while syncing.
  - v1 is unchanged.

## Tests Added / Run

- `validators/{validator_id}`:
  - `one_validator_by_pubkey_or_index_is_the_list_entry`
  - `one_unknown_validator_is_a_404`
  - `one_malformed_validator_id_is_a_400`
- v2 proposer duties:
  - `v2_proposer_duties_are_v1_s`
  - `the_v2_dependent_root_is_the_block_before_the_previous_epoch`
  - `the_v2_dependent_root_is_genesis_where_it_would_underflow`
  - `v2_proposer_duties_answer_503_while_syncing`
- `cargo test -p ethlambda-rpc --profile release-fast`: 136 passed, 0
failed.
- **Kurtosis devnet, Lighthouse v8.2.3's validator client talking only
to ethlambda beacon** (built from this branch). The setup:
- 2× geth+lighthouse hold validators [0, 112); Lighthouse's VC holds
[112, 128) with `--enable-doppelganger-protection`. With
`network_params_lighthouse_vc.yaml`'s split, where the VC holds every
key, the chain stops during the doppelganger wait (see below).
- Lighthouse skips doppelganger for validators registered at genesis, so
the VC was restarted at slot 40 to exercise it.

  | Check | Result |
  |---|---|
| Index resolution (`validators/{validator_id}`) | ✅ all 16 resolved;
before this PR: `Validator without index` for every key |
| v2 proposer duties | ✅ `current_epoch_proposers: 7`; it proposed slots
3, 4, 29 and 30. `dependent_root` matches Lighthouse's BN for epochs 0
and 1 |
  | Attestations and aggregates through ethlambda | ✅ published |
| `duties/sync` (lambdaclass#642) | ✅ `Validator in sync committee` for 112 and 114
|
| Doppelganger via `liveness` (lambdaclass#643) | ✅ after the restart: `Listening
for doppelgangers` (16), then `Found no doppelganger …
further_checks_remaining: 0` and `Doppelganger detection complete:
starting validator`. The validators unmuted at slot 128 (epoch 4) and
attested at once. `liveness` answered `false` for the muted keys and
`true` for a participant's |
  | Finality | ✅ justified 3, finalized 2 at epoch 4 |

- **Found on the devnet, not fixed here:**
- **Sync-committee endpoints missing.** The node doesn't serve
`sync_committee_subscriptions`, `pool/sync_committees` or the
contribution endpoints. Lighthouse logs `Unable to publish sync
committee messages` and `No aggregate contribution found` (CRIT) every
slot. This is the known gap lambdaclass#642 describes.
- **Duties refused at each epoch boundary.** `duties/attester` and
`duties/proposer` bound the requested epoch by the **head state's**
epoch, not the clock's (`validator.rs`, from the validator API work). At
an epoch's first slot, before its block arrives, the next epoch's duties
are a 400 (`epoch is not within one epoch of the head state's`).
Lighthouse retries next slot and gets them, but it logs an ERROR every
epoch. Lighthouse's BN advances the state instead; its own VCs logged no
such failure.
- **Stall with the committed split.**
`network_params_lighthouse_vc.yaml`'s split, where the VC holds all 128
keys, risks a stall: while doppelganger mutes the VC's validators, a run
of more than 8 missed slots puts Lighthouse past its sync tolerance and
every VC stops. An earlier run with a 64/64 split stopped at slot 55.
- **Proposer refused at the slot boundary.** Lighthouse's VC asked to
propose slot 29 a second time 3 ms before slot 30, once the block was
already imported; ethlambda answered `400 slot is not after the head
block`. Harmless: slashing protection would refuse the second signature
too.

## Related Issues / PRs

- Stacked on lambdaclass#643, which is stacked on lambdaclass#642. Retarget to
`beacon-chain-integration` once those merge.
- Unblocks the Lighthouse end-to-end check listed under "Not yet run" in
lambdaclass#642 and lambdaclass#643.

## ✅ Verification Checklist

- [x] Ran `make fmt` — clean
- [x] Ran `make lint` (clippy with `-D warnings`) — clean
- [ ] Ran `make test` (`test-consensus` plus `test-node`, at
`release-fast`) — only `ethlambda-rpc` locally (136 passed), the only
crate with code changes; the full suite runs in CI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beacon Ethereum Beacon Chain client

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants